Ранее мы уже рассказывали о NixOS, весьма необычном и удобном дистрибутиве GNU/Linux, позволяющем описать всю конфигурацию системы в одном файле и развернуть ее на любой машине в любой момент. Тогда мы дали лишь краткое введение в особенности системы и примеры ее использования. Сегодня я расскажу о том, как с помощью NixOS решать повседневные задачи и выходить из трудных ситуаций.

Упомяну, что значит NixOS для меня. Долгие годы я использовал Gentoo, и NixOS стал для меня Gentoo done right, когда, несмотря на гибкость системы и возможность простой сборки всего нужного из исходных кодов, я получил глобальный бинарный кеш для аналогичных базовым пакетов, а также простоту развертывания.

NixOS также служит отличным примером того, как нужно работать с комьюнити, — запросы на слияние (pull request) рассматриваются значительно быстрее и качественнее всех существующих на данный момент дистрибутивов. Даже если ты не занимаешься непосредственно разработкой, ты не столкнешься со старыми бюрократами из таких проектов, как Debian и Gentoo.

Из последнего следует один немаловажный и крайне приятный факт: ни оверлеи (overlay — сторонние репозитории, содержащие сценарии сборки ebuild для пакетов), ни их аналоги из других дистрибутивов больше не являются необходимостью. Отправляй свои патчи сразу в апстрим!

 

Готовые конфигурации

Так как NixOS предоставляет простую возможность распространять настройки своих машин так же, как и уже ставшая обычной традиция делиться своими конфигурациями для GNU Emacs, мы можем использовать подготовленные другими пользователями конфигурации в качестве базы для своей, тем самым не решая те проблемы, которые уже кто-то решал.

Для NixOS существует проект Simple Nixos Mailserver, который предоставляет готовую конфигурацию для почтового сервера. Полный пример конфигурации для этой статьи можно посмотреть на моем Git-сервере.

Мы можем включить чужую конфигурацию, забрав ее с любого ресурса (добавь эти строки в configuration.nix):

imports = [
  ...
  (builtins.fetchTarball {
    url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.2.0/nixos-mailserver-v2.2.0.tar.gz";
    sha256 = "0gqzgy50hgb5zmdjiffaqp277a68564vflfpjvk1gv6079zahksc";
  })
];

SHA-256 позволяет быть уверенным в аутентичности используемого архива.

Также вместо fetchTarball можно использовать fetchGit. Далее мы уже работаем с конфигурацией mailserver таким же образом, как если бы это было просто локальное включение файлов:

mailserver = {
  enable = true;
  fqdn = "mail.dumpstack.io";
  ...
};

Далее нам остается только (тут я намеренно опускаю настройку DNS, она не касается непосредственно настроек системы, и подобным придется заниматься что на NixOS, что на Ubuntu, что на других дистрибутивах) указать пользователей, alias’ы et cetera.

Таким же образом мы можем распространять что угодно, например управлять базовым слоем конфигурации для компании — настройкой домена, стандартных плагинов для браузера и так далее. Весомый плюс данного подхода — он не ограничивает свободу людей, работающих в компании, так как нет чувства, что твоим компьютером управляет кто-то другой: человек может посмотреть на обновленную конфигурацию, изменив URL «перейти на нее», и продолжать работать.

Или, быть может, тебе нужно работать со звуком и понадобилось реалтайм-аудио? Просто воспользуйся готовой конфигурацией от musnix.

 

Сборка пакета из исходников

Система сборки пакетов Nix чем-то напоминает систему ebuild для Gentoo, когда большую часть работы выполняет сам пакетный менеджер, который содержит информацию о том, как собирать при наличии Makefile, CMakeLists et cetera.

Сборочный файл для GNU Hello совсем не будет содержать указаний на то, как собирать:

{ stdenv, fetchurl }:

stdenv.mkDerivation rec {
  name = "hello-${version}";
  version = "2.10";

  src = fetchurl {
    url = "mirror://gnu/hello/${name}.tar.gz";
    sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i";
  };

  doCheck = true;

  meta = with stdenv.lib; {
    description = "A program that produces a familiar, friendly greeting";
    longDescription = ''
      GNU Hello is a program that prints "Hello, world!" when you run it.
      It is fully customizable.
    '';
    homepage = https://www.gnu.org/software/hello/manual/;
    license = licenses.gpl3Plus;
    maintainers = [ maintainers.eelco ];
    platforms = platforms.all;
  };
}

Для сборки и установки достаточно записать эти строки в файл (hello.nix, например) и выполнить следующую команду:

$ nix-env -r -f ./hello.nix -i

Если понадобится совершить какие-либо нестандартные действия (частично это будет рассмотрено на примере работы с бинарными пакетами далее), достаточно переопределить конкретные этапы.

Мы не будем подробно рассматривать все случаи сборки различных приложений, ибо вариантов бесчисленное множество. Тем не менее я советую посмотреть своими глазами на то, каким образом составляются описания для сборки различного ПО в репозитории nixpkgs. Я крайне редко (буду честным — никогда) пишу описания сборки с нуля, в nixpkgs легко найти пример для разных систем сборки, а также великолепный набор костылей на все случаи жизни.

 

Бинарные пакеты и FHS

NixOS отличается от других дистрибутивов тем, что разработчикам дистрибутива пришлось отказаться от совместимости с FHS — стандартом, определяющим местоположение библиотек, исполняемых файлов, конфигурации и прочего. Из-за этого бинарные утилиты, собранные для других дистрибутивов, без изменений работать не смогут: каталогов /usr/share и /usr/lib в NixOS попросту нет.

При этом, если разработчик проприетарного программного обеспечения будет собирать для Nix, ему это будет только в плюс, так как сам пакетный менеджер Nix на данный момент работает на всех дистрибутивах (включая Ubuntu, Fedora, Debian), а простая работа с зависимостями позволит не беспокоиться о том, заработает ли оно на конкретном.

Например, для создания пакета для BinaryNinja нам необязательно костылить с изменением бинарных файлов и зависимостей вручную (за исключением libpython, но это уже не зависит непосредственно от Nix).

with import <nixpkgs> {};

pkgs.stdenv.mkDerivation {
  name = "binary-ninja";

  src = ./BinaryNinja.zip;

  installPhase = ''
    mkdir -p $out/share/binary-ninja
    cp -r * $out/share/binary-ninja
    ln -s ${pkgs.python2}/lib/libpython2.7.so.1.0 $out/share/binary-ninja/libpython2.7.so.1

    mkdir -p $out/bin
    ln -s $out/share/binary-ninja/binaryninja $out/bin/
  '';

  nativeBuildInputs = [ autoPatchelfHook ];
  buildInputs = [
    unzip
    stdenv.cc.cc glib fontconfig dbus libglvnd
    xlibs.libX11 xlibs.libXi xlibs.libXrender
    python2
  ];
}

Пояснения излишни — мы имеем некоторый бойлерплейт описания сборки, а также список зависимостей. Далее достаточно выполнить команду

$ nix-env -r -f ./binaryninja.nix -i

После чего binaryninja будет установлен на уровне пользователя, но с управлением через пакетный менеджер.

Но есть нюанс, который нужно помнить: в том случае, если это бинарное ПО, которое хочет обновляться самостоятельно (как binaryninja), оно это сделать не сможет, так как каталог приложения при установке через пакетный менеджер доступен только для чтения.


 

Приятные мелочи

Для Chromium существует возможность управлять плагинами со стороны системы (небольшое описание есть тут).

NixOS позволяет управлять плагинами для Chromium прямо в конфигурации для системы. Например, чтобы установить по умолчанию HTTPS Everywhere и uBlock Origin, достаточно всего лишь определить их в списке extensions:

programs.chromium = {
  enable = true;
  extensions = [
    "cjpalhdlnbpafiamejdnhcphjbkeiagm" # uBlock Origin
    "gcbommkclmclpchllfjekcdonpmejbdp" # HTTPS Everywhere
  ];
};
 

Hardened

Для NixOS существует профиль Hardened, который объединяет в себе полезные для безопасности настройки системы. И у тебя есть возможность откатить некоторые из них. Например, я делаю так:

imports =
  [ # Include the results of the hardware scan.
    ./hardware-configuration.nix
    <nixpkgs/nixos/modules/profiles/hardened.nix>
  ];

security.allowUserNamespaces = true;
security.allowSimultaneousMultithreading = true;

В данном случае я использую hardened-опции по умолчанию, но некоторые из них возвращаю в нужное мне состояние.

Значительно большего уровня защищенности можно достигнуть использованием специализированного hardened-ядра.

Все hardened-ядра в NixOS имеют постфикс _hardened, соответственно, возможные варианты можно найти простым поиском по пакетам (также я добавляю .kernel, так как по умолчанию nix search покажет и модули ядра):

$ nix search linuxPackages '_hardened\.kernel'

К примеру, на момент написания статьи были доступны следующие ядра:

* nixpkgs.linuxPackages_hardened.kernel (linux-4.14.105)
* nixpkgs.linuxPackages_latest_hardened.kernel (linux-5.0)
* nixpkgs.linuxPackages_latest_xen_dom0_hardened.kernel (linux-5.0)
* nixpkgs.linuxPackages_testing_hardened.kernel (linux-5.0-rc8)
* nixpkgs.linuxPackages_xen_dom0_hardened.kernel (linux-4.14.105)

Аналогичным образом можно искать и обычные ядра:

$ nix search linuxPackages '\.kernel'

Стоит отметить, что hardened-ядра не включают опции из профиля Hardened по умолчанию, тем самым гибкость снижается только для той конфигурации ядра, которую невозможно изменять после загрузки посредством интерфейсов ядра.

Далее в configuration.nix указываем необходимое нам ядро, например:

boot.kernelPackages = pkgs.linuxPackages_latest_hardened;

После чего ставший уже привычным nixos-rebuild switch и в случае со сменой ядра — перезагрузка (или kexec).

 

Хранение секретов для публичных конфигураций

Самый простой способ для того случая, когда ты распространяешь конфигурацию, — использовать включение конфигурации через import. Например:

let
  secrets = import ./secrets.nix;
in {
  users.extraUsers.root = {
    openssh.authorizedKeys.keys = [ secrets.pubkey ];
  };
 

Декларативные контейнеры

Большинство из нас знает, многие любят и постоянно применяют, а некоторые даже изобретают свои системы контейнерной виртуализации, позволяющие запускать отдельные приложения или целые операционные системы, используя ядро хостовой ОС.

Что меня восхищает в контейнерах в NixOS — возможность использовать те же самые декларации, которые использовались ранее для хостовой системы. По сути, единственное, что отличает контейнер от основной системы, — другое пространство имен (namespace) в конфигурационном файле.

В качестве примера покажу одну из деклараций контейнера, которая у меня осталась где-то в конфигурациях. В данном случае определен контейнер с версией Chromium из релиза NixOS 18.03.

containers.test = {
  autoStart = true;
  config =
  { config, pkgs, ... }:
  let
    stableTarball = fetchTarball "${tarball-base}/nixos-18.03.tar.gz";
  in {
    nixpkgs.config = {
      allowUnfree = true;
      packageOverrides = pkgs: {
        stable = import stableTarball {
          config = config.nixpkgs.config;
        };
      };
    };
    environment.systemPackages = with pkgs; [
      stable.chromium
    ];
  };
};

То есть, как ты уже мог заметить, только небольшой «контейнерный бойлерплейт», и мы получаем логически изолированную среду. При этом стоит отметить, что контейнеры в NixOS не предполагают изоляцию с целью усиления безопасности.

 

Декларативная генерация виртуальных машин

Дам ссылку на свою предыдущую статью про AppVM, которую я писал, еще когда пользовал Gentoo в качестве хостовой системы.

Использование виртуальных машин в рамках только пакетного менеджера Nix в других дистрибутивах и использование Nix в рамках NixOS в данном случае ничего не изменит. Тут можно продолжить оды Nix за внесение универсальности не только в рамках «материнского дистрибутива», но и в общем в экосистеме GNU/Linux, но буду придерживаться основной цели статьи и показывать больше непосредственно примеры использования в повседневной жизни.

 

Стабильность

Как-то раз я случайно обнаружил, что поставил новую систему из unstable-ветви. В NixOS unstable-ветвь собирается из тех пакетов, которые добавили непосредственно в Git-репозиторий nixpgs на GitHub и которые после успешно собрались. Тем самым я оказался на весьма суровом bleeding edge.

Очевидным результатом стало то, что однажды (на самом деле это было в первый день) я все-таки отхватил баг — virt-manager не работал.


Что я мог бы сделать, если бы использовал классические дистрибутивы? Добавить репозиторий со старыми пакетами, установить низкий приоритет, попробовать установить пакет и после — начать страдать от безысходности из-за того, что прошлая версия пакета требует одной версии определенной библиотеки, а другой установленный пакет в системе — другой версии этой же библиотеки.

В случае NixOS подобных проблем быть не может. Вопрос решился добавлением стабильной ветки:

nixpkgs.config = {
  allowUnfree = true;
  packageOverrides = pkgs: {
    stable = import (fetchTarball "${tarball-base}/nixos-18.09.tar.gz") {
      config = config.nixpkgs.config;
    };
  };
};

где tarball-base определяется ранее:

let
  tarball-base = "https://github.com/NixOS/nixpkgs-channels/archive/";
in {
  ...

После чего в списке используемых пакетов мне достаточно использовать префикс stable. для того, чтобы устанавливать пакеты из стабильных ветвей, например:

environment.systemPackages = with pkgs; [
  ...
  stable.virt-manager
  ...
];

Для простоты понимания того, какая версия используется, есть возможность указывать симлинки явно, например:

environment.systemPackages = with pkgs; [
  ...
  (pkgs.writeShellScriptBin "virt-manager-unstable" "${virtmanager}/bin/virt-manager $@")
  (pkgs.writeShellScriptBin "virt-manager-stable" "${stable.virtmanager}/bin/virt-manager $@")
];

Таким же образом можно использовать любой из снимков состояния репозитория nixpkgs для того, чтобы получать нужные версии.

Например, мы хотим найти версию снимка состояния репозитория nixpgs, в котором virt-manager имел версию 2.0.

Для этого нам достаточно перейти в каталог с нужным пакетом:

$ cd nixpkgs/pkgs/applications/virtualization/virt-manager

После чего, запустив git log, найти коммит (простым поиском по номеру версии), в котором данная версия была добавлена. Например, virt-manager 2.0 был добавлен в этом коммите:

commit db7e9408a1b5c0d7d103c28b91646b374968b045
Author: Gabriel Ebner gebner@gebner.org
Date: Wed Nov 7 22:25:09 2018 +0100

  virtmanager: 1.5.1 -> 2.0.0

Следовательно, для получения virt-manager версии 2.0 нам достаточно использовать архив с этого коммита:

nixpkgs.config = {
  allowUnfree = true;
  packageOverrides = pkgs: {
    stable = import (fetchTarball "${tarball-base}/nixos-18.09.tar.gz") {
      config = config.nixpkgs.config;
    };
    commit1 = import (fetchTarball "${tarball-base}/db7e9408a1b5c0d7d103c28b91646b374968b045.tar.gz") {
      config = config.nixpkgs.config;
    };
  };
};

После мы можем таким же образом, как и ранее, добавить в bin исполняемый файл для определенной версии.

environment.systemPackages = with pkgs; [
  ...
  (pkgs.writeShellScriptBin "virt-manager-unstable" "${virtmanager}/bin/virt-manager $@")
  (pkgs.writeShellScriptBin "virt-manager-stable" "${stable.virtmanager}/bin/virt-manager $@")
  (pkgs.writeShellScriptBin "virt-manager-2.0.0" "${commit1.virtmanager}/bin/virt-manager $@")
];

Итого мы имеем virt-manager из последней стабильной ветки 18.09, той версии, которая на данный момент находится в Git, а также версии 2.0.0.

Это подход со стороны основной конфигурации системы. Таким образом можно определять каждую из версий пакетов, но значительный минус данного подхода в том, что каждый архив должен быть загружен, соответственно есть некоторый оверхед по размеру файлов.

Альтернативный вариант — установка Nix-пакета от пользователя, в этом случае достаточно просто запустить команду в каталоге с описанием для сборки.

Или же можно вернуться на стабильную ветку из unstable. Предположим, мы хотим переключиться на следующий релиз 19.03, для этого достаточно сделать так:

$ nix-channel --remove nixos-19.03
$ nix-channel --add  https://nixos.org/channels/nixos-19.03 nixos
$ nixos-rebuild switch --upgrade

Проблемы с даунгрейдом системы целиком? В разумных пределах не должны быть, а те, что будут, связаны с конфигурацией из твоего домашнего каталога, но никак не с базовой системой.

Каналы также можно использовать напрямую, например если мы добавим каналы для стабильного релиза, нестабильного и следующего планируемого стабильного.

$ nix-channel --add https://nixos.org/channels/nixos-19.03 nixos
$ nix-channel --add https://nixos.org/channels/nixos-18.09 stable
$ nix-channel --add https://nixos.org/channels/nixos-unstable unstable
$ nix-channel --update

Тот, что назван nixos, будет использоваться по умолчанию.

Далее мы можем определить каналы и использовать их для установки программ:

let
  unstable = import <unstable> {};
  stable = import <stable> {};
in {
  ...
  environment.systemPackages = with pkgs; [
    ...
    unstable.chromium
  ];
  ...
}

Для того чтобы не пропустить улучшения в плане работы с тем, что было описано выше, советую заглянуть в статью на NixOS Wiki.

 

Более радикальное решение проблем с FHS

Проблема несовместимости NixOS с FHS в основном затрагивает бинарные пакеты (суть проблемы я описал в соответствующем разделе), но часто бывает, что написанный очень странными людьми код собирается, только если в системе есть условный /usr/lib/libastral.so по жестко заданному пути, и вне FHS-совместимого окружения собирать становится невыносимо.

Решение с использованием Docker в этом случае довольно очевидно и приземленно, поэтому оставим его хипстерам. Для своих задач я предпочитаю использовать обычное chroot-окружение с Debian, и я покажу, как его готовить таким образом, чтобы он не доставлял проблем.

Для начала просто генерируем chroot с Debian или любым другим дистрибутивом, который мы будем использовать для FHS окружения (я использую Debian и Gentoo):

$ cd /home/user/chroots
$ debootstrap sid sid-root http://deb.debian.org/debian/

После чего добавляем в configuration.nix сервис с mount-костылями, который будет монтировать все нужные нам каталоги. Самое важное из этого — привязка (bind) домашнего каталога хостовой системы к системе в chroot.

systemd = {
  services = {
    "sid-chroot-mounts" = {
      enable = true;
      description = "Setup mounts for debian sid chroot";
      wantedBy = [ "multi-user.target" ];
      script = ''
        ls /home/user/chroots/sid-root/home/user/.zshrc && exit
        ${pkgs.utillinux}/bin/mount --bind /home/user /home/user/chroots/sid-root/home/user 
        ${pkgs.utillinux}/bin/mount --bind /dev /home/user/chroots/sid-root/dev
        ${pkgs.utillinux}/bin/mount --bind /proc /home/user/chroots/sid-root/proc
        ${pkgs.utillinux}/bin/mount --bind /sys /home/user/chroots/sid-root/sys
      '';
      serviceConfig.Type = "oneshot";
    };
  };
};

Далее добавляем в скрипт инициализацию используемого шелла (у меня это .zshrc):

$ alias fhs="su -c 'chroot /home/user/chroots/sid-root /bin/su -l user'"

Теперь достаточно набрать команду fhs, и мы окажемся в окружении Debian, использующем наш родной домашний каталог. Разве не прекрасно?

Далее, для того чтобы не перепутать окружения, я советую добавить небольшую проверку в тот же файл инициализации шелла.

$ which apt >/dev/null 2>&1 && PS1="fhs-chroot $PS1"

Что мы получили в итоге?

user@local ~ $ fhs
Password:
debian user@local ~ $

 

Home manager

Некоторые пользователи идут дальше и используют такой же подход к конфигурированию не только системы, но и домашнего каталога пользователя. Тут стоит отметить, что это будет эффективно не только на NixOS, так как home-manager от Rycee работает на уровне пользователя.

Например, Nix + home-manager можно использовать на macOS.

 

Чем ты можешь помочь NixOS?

На данный момент активна инициатива Zero Hydra Failures, которая предполагает устранение всех ошибок к следующему релизу 19.03.

 

Где получить помощь?

Англоязычную поддержку можно получить в канале #nixos на freenode, полный список каналов можно найти в списке c NixOS wiki.

Если тебе нужна поддержка на русском языке, можешь создать треды с тегами nix и nixos на ЛОРе, где на данный момент отвечают некоторые из русскоязычных разработчиков NixOS.

 

Вместо заключения

Сегодня я приоткрыл часть повседневности пользователя NixOS. Можно видеть, что NixOS значительно упрощает базовый слой системы в рамках конфигурирования, а также позволяет не изобретать велосипед при использовании одной конфигурации на разных машинах, даже когда это не просто разные дистрибутивы, а разные операционные системы.

Оцени статью:

Что тебе понравилось больше всего?
Что тебе не понравилось больше всего?